﻿using System;
using System.Text;
using System.Diagnostics;
using System.Xml;
using Engine.Dtd.Schema;
using System.Text.RegularExpressions;
using Engine.Sgml.Configuration;
using System.IO;
using System.Collections.Generic;
using Engine.Sgml.Validation;
using Engine.Sgml.Validation.Rules;

namespace Engine.Sgml.Parser {
    [DebuggerDisplay("{streamReader.Context()}")]
    internal sealed class SgmlParser {
        private SgmlDocument document;
        private SgmlStreamReader streamReader;
        private IDictionary<SgmlNodeType, IElementValidator> validators;

        private Context context;
        public Context Context {
            get { return context; }
            }

        public SgmlParser(SgmlDocument document, ValidationRuleCollection rules) {
            Debug.Assert(document != null);
            Debug.Assert(document.Schema != null);
            Debug.Assert(document.TraceSource != null);
            Guard.ArgumentNotNull(document, "document");

            this.document = document;
            this.context = new Context(document.ValidatorBehavior, document.Schema, document.TraceSource);
            this.streamReader = new SgmlStreamReader();

            validators = new Dictionary<SgmlNodeType, IElementValidator>();
            validators.Add(SgmlNodeType.Element, new StartElementValidator(rules, document.TraceSource));
            validators.Add(SgmlNodeType.EndElement, new EndElementValidator(rules));
            validators.Add(SgmlNodeType.Document, new RootElementValidator(document));
            validators.Add(SgmlNodeType.DocumentType, new DoctypeValidator(document));
            validators.Add(SgmlNodeType.ProcessingInstruction, new ProcessingInstructionValidator(document));
            validators.Add(SgmlNodeType.WhiteSpace, new IgnoreWhiteSpaceValidator());

            document.ValidatorBehavior.Changed += (object sender, BehaviorChangedEventArgs e) => {
                ValidatorBehavior behavior = (ValidatorBehavior)sender;
                switch (e.Name) {
                    case "IgnoreComments":
                        if (behavior.IgnoreComments)
                            validators.Add(SgmlNodeType.Comment, new IgnoreCommentValidator());
                        else
                            validators.Remove(SgmlNodeType.Comment);
                        break;
                    case "PreserveWhiteSpace":
                        validators.Remove(SgmlNodeType.WhiteSpace);
                        if (behavior.PreserveWhitespace) {
                            if (!behavior.DtdDisabled)
                                validators.Add(SgmlNodeType.WhiteSpace, new WhiteSpaceValidator());
                            }
                        else
                            validators.Add(SgmlNodeType.WhiteSpace, new IgnoreWhiteSpaceValidator());
                        break;
                    case "DtdDisabled":
                        if (behavior.DtdDisabled) {
                            rules.Clear();
                            rules.Add(ValidationRuleCollection.EmptyRule);
                            if (behavior.PreserveWhitespace)
                                validators.Remove(SgmlNodeType.WhiteSpace);
                            else
                                Debug.Assert(validators.ContainsKey(SgmlNodeType.WhiteSpace) && validators[SgmlNodeType.WhiteSpace] is IgnoreWhiteSpaceValidator);
                            }
                        else {
                            // Initialize validation rules
                            rules.Initialize(document.Schema, behavior);
                            if (behavior.PreserveWhitespace)
                                validators.Add(SgmlNodeType.WhiteSpace, new WhiteSpaceValidator());
                            else
                                Debug.Assert(validators.ContainsKey(SgmlNodeType.WhiteSpace) && validators[SgmlNodeType.WhiteSpace] is IgnoreWhiteSpaceValidator);
                            }
                        break;
                    default:
                        break;
                    }
            };
            }

        public void Parse(TextReader stream) {
            Guard.ArgumentNotNull(stream, "stream");

            streamReader.Initialize(stream);
            streamReader.ReadChar();
            streamReader.SkipWhitespace();

            SgmlNodeType nodeType;
            IElementReader reader;
            IElementValidator validator;
            IDictionary<SgmlNodeType, IElementReader> readers = context.Readers;

            context.Schema = document.Schema;
            SElement parent = document.Root;
            while (Read(parent, out nodeType)) {
                if (readers.TryGetValue(nodeType, out reader)) {
                    SElement element = reader.Read(streamReader, parent, context);
                    if (validators.TryGetValue(element.NodeType, out validator))
                        parent = validator.Validate(element, parent);
                    }
                else
                    throw new InvalidOperationException(string.Format("Could not found an appropriate reader for '{0}' node.", nodeType));
                }
            if (validators.TryGetValue(document.Root.NodeType, out validator))
                validator.Validate(document.Root, parent);
            }

        private bool Read(SElement parent, out SgmlNodeType nodeType) {
            //Debug.Assert(entityReader.LineNumber != 2138, "The specified line number was found.");
            context.SetLineInfo(streamReader);
            nodeType = DetectNodeType(streamReader, parent);
            if (nodeType == SgmlNodeType.None)
                return false;
            else
                return true;
            }

        public static SgmlNodeType DetectNodeType(SgmlStreamReader reader, SElement parent) {
            SgmlNodeType nodeType;
            switch (reader.CurrentChar) {
                case Characters.EndOfFile:
                    nodeType = SgmlNodeType.None;
                    break;
                case Characters.StartTagMarkup:
                    if (parent.HasDeclaration && parent.Declaration.ContentModel.TextOnly) {
                        nodeType = SgmlNodeType.Text;
                        break;
                        }
                    switch (reader.ReadChar()) {
                        case Characters.ExclamationPoint:
                            switch (reader.ReadChar()) {
                                case Characters.CommentMarkup:
                                    nodeType = SgmlNodeType.Comment;
                                    break;
                                case Characters.LeftBracket:
                                    nodeType = SgmlNodeType.CDataSection;
                                    break;
                                default:
                                    if (XmlConvert.IsStartNCNameChar(reader.SkipWhitespace()))
                                        nodeType = SgmlNodeType.DocumentType;
                                    else
                                        nodeType = SgmlNodeType.Comment;
                                    break;
                                }
                            break;
                        case Characters.ProcessingInstructionMark:
                            nodeType = SgmlNodeType.ProcessingInstruction;
                            break;
                        case Characters.ClosingTagMarkup:
                            nodeType = SgmlNodeType.EndElement;
                            break;
                        default:
                            nodeType = SgmlNodeType.Element;
                            break;
                        }
                    break;
                case Characters.Whitespace:
                case Characters.HorizontalTab:
                case Characters.CarriageReturn:
                case Characters.LineFeed:
                    nodeType = SgmlNodeType.WhiteSpace;
                    break;
                default:
                    nodeType = DetetectNodeType(parent);
                    break;
                }
            return nodeType;
            }

        private static SgmlNodeType DetetectNodeType(SElement parent) {
            SgmlNodeType nodeType;
            if (parent.Declaration == null || parent.Declaration.ContentModel.TextOnly)
                nodeType = SgmlNodeType.Text;
            else
                switch (parent.Declaration.ContentModel.ContentType) {
                    case DContentModelType.Default:
                        nodeType = SgmlNodeType.Text;
                        break;
                    case DContentModelType.CData:
                        nodeType = SgmlNodeType.CData;
                        break;
                    case DContentModelType.RCData:
                        nodeType = SgmlNodeType.Text;
                        break;
                    case DContentModelType.Empty:
                        nodeType = SgmlNodeType.Text;
                        break;
                    default:
                        throw new InvalidOperationException("Unknown the content type of a model.");
                    }
            return nodeType;
            }

        public static SgmlNodeType IsText(char ch, SElement parent) {
            Debug.Assert(parent != null);
            switch (ch) {
                case Characters.EndOfFile:
                    return SgmlNodeType.None;
                case Characters.StartTagMarkup:
                    return SgmlNodeType.None;
                default:
                    return DetetectNodeType(parent);
                }
            }
        }
    }